using System;

namespace ICommonLibrary.Date {

    public sealed class MutableDate : IComparable, IComparableDate {

        /// <summary>
        /// Returns a value between 1 and 9999.
        /// </summary>
        public int Year {
            get { return _year; }
            set {
                _year = value;
                Normalise();
            }
        }

        /// <summary>
        /// Returns a value between 0 and 11.
        /// </summary>
        public int Month {
            get { return _month; }
            set {
                _month = value;
                Normalise();
            }
        }

        /// <summary>
        /// Returns a value between 1 and 31.
        /// </summary>
        public int Date {
            get { return _date; }
            set {
                _date = value;
                if (_date > 28 || _date < 1) {
                    Normalise();
                }
                else {
                    _uniqueKey = GenerateUniqueKey(_year, _month, _date);
                }
            }
        }

        private int _year;
	    private int _month;
	    private int _date;
	    private int _uniqueKey;
    	
	    public MutableDate() : this(DateTime.Now) {
	    }
    	
        /// <summary>
        /// 
        /// </summary>
        /// <param name="year">1-9999</param>
        /// <param name="month">0-11</param>
        /// <param name="date">1-31</param>
	    public MutableDate(int year, int month, int date) {
		    _year = year;
		    _month = month;
		    _date = date;
    		
		    Normalise();
	    }
    	
	    public MutableDate(DateTime dateTime) : this(dateTime.Year, dateTime.Month - 1, dateTime.Day) {
	    }

        public static MutableDate FromUniqueKey(int uniqueKey) {
            return new MutableDate(uniqueKey / 10000, (uniqueKey % 10000) / 100, uniqueKey % 100);
        }
    	
        private MutableDate(int year, int month, int date, int uniqueKey) {
		    _year = year;
		    _month = month;
		    _date = date;
		    _uniqueKey = uniqueKey;
	    }

	    public MutableDate Clone() {
		    return new MutableDate(_year, _month, _date, _uniqueKey);
	    }

        public int CompareTo(object o) {
            if (o == null) {
                return 1;
            }
            if (o == this) {
                return 0;
            }
            var d = o as MutableDate;
            if (d == null) {
                return 1;
            }

            return _uniqueKey - d._uniqueKey;
        }

        public bool Before(MutableDate d) {
		    return _uniqueKey < d._uniqueKey;
	    }
    	
	    public bool After(MutableDate d) {
		    return _uniqueKey > d._uniqueKey;
	    }
    	
	    public MutableDate AddDays(int n) {
		    _date += n;
		    if (_date > 28 || _date < 1) {
			    Normalise();
		    }
		    else {
			    _uniqueKey = GenerateUniqueKey(_year, _month, _date);
		    }
		    return this;
	    }
    	
	    public MutableDate AddMonths(int months) {
		    _month += months;
		    Normalise();
		    return this;
	    }
    	
	    public MutableDate AddYears(int years) {
		    _year += years;
		    Normalise();
		    return this;
	    }

        public MutableDate SetYear(int year) {
            Year = year;
            return this;
        }

        public MutableDate SetMonth(int month) {
            Month = month;
            return this;
        }

	    public MutableDate SetDate(int date) {
	        Date = date;
		    return this;
	    }
    	
	    public int GetUniqueKey() {
		    return _uniqueKey;
	    }
 
	    public int HashCode() {
		    return _uniqueKey;
	    }
    	
	    public bool Equals(MutableDate d) {
		    return _uniqueKey == d._uniqueKey;
	    }

        public override int GetHashCode() {
            return _uniqueKey;
        }

        public override bool Equals(Object o) {
		    if (o == this) return true;
		    if (o != null && o.GetType() == typeof(MutableDate)) {
			    var d = (MutableDate) o;
			    return _uniqueKey == d._uniqueKey;
		    }
		    return false;
	    }

	    public DateTime ToDateTime() {
		    return new DateTime(_year, _month + 1, _date);
	    }

        public DateTime ToDateTime(int hour, int minute, int second) {
            return new DateTime(_year, _month + 1, _date, hour, minute, second);
        }

        public override string ToString() {
            return _year + "-" + (_month + 1) + "-" + _date;
        }

        /**
	     * This method may contains some boilerplate code that is to help it run as
	     * fast as it can.
	     */
	    private void Normalise() {

		    // normalise month
		    if (_month > 11 || _month < 0) {
			    NormaliseMonth();
		    }
    		
		    // normalise date
		    int y = _year;
		    int m = _month;
		    int d = _date;
		    bool leap = DateUtil.IsLeapYear(y);
    		
		    if (d > 28)  {
                int daysOfMonth = leap ? DateUtil.LeapYearDays[m] : DateUtil.NormalYearDays[m];
			    while (d > daysOfMonth) {
				    m = ++_month;
				    if (m > 11 || m < 0) {
                        m = NormaliseMonth(); // NormaliseMonth may change month
				    }
				    d -= daysOfMonth;
    				
				    if (y != _year) {
					    y = _year;
                        leap = DateUtil.IsLeapYear(y);
				    }
                    daysOfMonth = leap ? DateUtil.LeapYearDays[m] : DateUtil.NormalYearDays[m];
			    }
		    }
    		
		    while (d <= 0) {
			    m = --_month;
			    if (m > 11 || m < 0) {
				    m = NormaliseMonth();
			    }
			    if (y != _year) {
				    y = _year;
                    leap = DateUtil.IsLeapYear(y);
			    }
                d += leap ? DateUtil.LeapYearDays[m] : DateUtil.NormalYearDays[m];
		    }
    		
		    _date = d;
		    _uniqueKey = GenerateUniqueKey(y, m, d);
	    }

	    private static int GenerateUniqueKey(int y, int m, int d) {
		    return y * 10000 + m * 100 + d;
	    }

	    private int NormaliseMonth() {
		    int m = _month;
		    if (m > 11) {
			    int delta = m % 12;
			    _year += m / 12;
			    m = _month = delta;
		    }
		    else if (m < 0) {
			    int delta = m % 12;
			    _year += m / 12;
			    if (delta != 0) {
				    _year--;
				    m = _month = 12 + delta;
			    }
			    else {
				    m = _month = 0;
			    }
		    }
		    return m;
	    }
            
    }
}